package uk.ac.lkl.common.allocator;



import java.util.*;

import uk.ac.lkl.common.allocator.event.*;

import uk.ac.lkl.common.util.ds.AbstractNotifyingList;
import uk.ac.lkl.common.util.ds.NotifyingList;

//import uk.ac.lkl.common.util.ChangeSupport;


/**
 * The model for a schedule.
 * 
 * @author Darren Pearce
 * @version $Revision: 625 $
 * @version $Date: 2008-02-22 14:45:30 +0000 (Fri, 22 Feb 2008) $
 * 
 */
public class AllocatorModel<O> {

    private AllocatorSupport<O> support = new AllocatorSupport<O>(this);
//    private ChangeSupport cSupport = new ChangeSupport(this);
    private int numColumns;
    
    private AbstractNotifyingList<O> collection;


    /**
     * Map from object to HashMap of counts.
     * 
     */
    private HashMap<O, ArrayList<Integer>> allocationMap = new HashMap<O, ArrayList<Integer>>();


    private String title;
//    private String unit;

    private String timeUnit;


    public AllocatorModel(Class<O> elementClass, int numColumns) {
        this(numColumns, new NotifyingList<O>(elementClass));
    }


    public AllocatorModel(int numColumns, AbstractNotifyingList<O> collection) {
        this.numColumns = numColumns;
        this.collection = collection;
        initialiseBlockLists();
    }


    public AbstractNotifyingList<O> getList() {
        return collection;
    }


    public int getNumColumns() {
        return numColumns;
    }


    public Collection<O> getObjects() {
        return allocationMap.keySet();
    }


    private void initialiseBlockLists() {
        for (O object : collection)
            initialiseAllocations(object);
    }


    /**
     * Returns true if successfully added.
     * 
     * @param object
     * @return
     * 
     */
    public boolean addObject(O object) {
        ArrayList<Integer> allocations = getModifiableAllocations(object);

        if (allocations == null) {
            initialiseAllocations(object);
            return true;
        }

        return false;
    }


    /**
     * Sets the title of this allocation
     * 
     * @author Uri
     * @param String
     *            title
     * @return
     */
    public void setTitle(String title) {
        this.title = title;
    }


    public String getTitle() {
        return title;
    }


    /**
     * Sets the time units for this allocation (IE weeks, hours, minutes,
     * nanoseconds,whatever. This should be taken from the plan properties,
     * (plan-time-unit)
     */
    public void setTimeUnit(String unit) {
        this.timeUnit = unit;
    }


    public String getTimeUnit() {
        return this.timeUnit;
    }


    /**
     * Returns an (unmodifiable) list of the current allocations.
     * 
     * @param object
     * @return
     * 
     */
    public List<Integer> getAllocations(O object) {
        ArrayList<Integer> allocations = getModifiableAllocations(object);
        if (allocations == null)
            allocations = initialiseAllocations(object);
        return Collections.unmodifiableList(allocations);
    }


    public int getAllocation(O object, int index) {
        List<Integer> allocations = getAllocations(object);
        int allocation = allocations.get(index);
        return allocation;
    }


    private ArrayList<Integer> getModifiableAllocations(O object) {
        ArrayList<Integer> allocations = allocationMap.get(object);

        if (allocations == null)
            return null;

        return allocations;
    }


    private ArrayList<Integer> initialiseAllocations(O object) {
        // hack: can't do this since model listens to collection. Get
        // event loop if have this here
        // All actions should be done through collection
        // collection.add(object);

        ArrayList<Integer> allocations = new ArrayList<Integer>();

        for (int i = 0; i < numColumns; i++)
            allocations.add(0);

        allocationMap.put(object, allocations);
        return allocations;
    }


    public boolean addAllocation(O object, int index) {
        return increase(object, index);
    }


    /**
     * Returns true only if successfully adds all blocks.
     * 
     * @param object
     * @param start
     * @param end
     * @return
     * 
     */
    public boolean allocate(O object, int start, int end) {
        int low = Math.min(start, end);
        int high = Math.max(start, end);
        boolean result = true;
        for (int i = low; i <= high; i++) {
            boolean ok = addAllocation(object, i);
            if (!ok)
                result = false;
        }
        return result;
    }


    public void unallocate(O object, int start, int end) {
        int low = Math.min(start, end);
        int high = Math.max(start, end);
        for (int i = low; i <= high; i++)
            unallocate(object, i);
    }


    public boolean changeAllocation(O object, int index, int change) {
        ArrayList<Integer> allocations = getModifiableAllocations(object);

        if (allocations == null)
            allocations = initialiseAllocations(object);

        int oldAllocation = allocations.get(index);

        int newAllocation = oldAllocation + change;

        if (newAllocation < 0)
            return false;

        if (newAllocation == oldAllocation)
            return false;

        allocations.set(index, newAllocation);

        support.fireAllocationChanged(new AllocatorEvent<O>(this, object,
                index, oldAllocation));
        return true;
    }


    public boolean unallocate(O object, int index) {
        return setAllocation(object, index, 0);
    }


    public boolean setAllocation(O object, int index, int newAllocation) {
        if (newAllocation < 0)
            return false;

        ArrayList<Integer> allocations = getModifiableAllocations(object);

        if (allocations == null)
            allocations = initialiseAllocations(object);

        int oldAllocation = allocations.get(index);

        if (newAllocation == oldAllocation)
            return false;

        allocations.set(index, newAllocation);

        support.fireAllocationChanged(new AllocatorEvent<O>(this, object,
                index, oldAllocation));
        return true;

    }


    public boolean pushSetAllocation(O object,
                                     int index,
                                     int newAllocation,
                                     boolean positiveDirection) {
        // if allocation for index is currently zero then no need to
        // push
        if (newAllocation < 0)
            return false;

        ArrayList<Integer> allocations = getModifiableAllocations(object);

        if (allocations == null)
            allocations = initialiseAllocations(object);

        int oldAllocation = allocations.get(index);

        if (oldAllocation == 0)
            return setAllocation(object, index, newAllocation);

        // if (newAllocation == currentAllocation)
        // return false;

        int step = positiveDirection ? 1 : -1;
        int limit = positiveDirection ? numColumns - 1 : 0;

        // if is last valid index, can't push
        if (index == limit)
            return false;

        // find a space
        boolean spaceFound = false;
        int spaceIndex = index;
        // look for an unallocated index
        for (int i = index + step; i != limit + step; i += step) {
            int allocation = allocations.get(i);
            if (allocation == 0) {
                spaceFound = true;
                spaceIndex = i;
                break;
            }
        }

        // do nothing if no space found
        if (!spaceFound) {
            return false;
        }


        // push
        for (int i = spaceIndex; i != index; i -= step) {
            int allocation = allocations.get(i - step);
            setAllocation(object, i, allocation);
        }

        allocations.set(index, newAllocation);
        support.fireAllocationChanged(new AllocatorEvent<O>(this, object,
                index, oldAllocation));

        return true;
    }


    public boolean increase(O object, int index) {
        return changeAllocation(object, index, 1);
    }


    public boolean decrease(O object, int index) {
        return changeAllocation(object, index, -1);
    }


    /**
     * Get all the blocks that overlap with this one and push them out the way.
     * 
     * @param block
     * 
     */
    // private boolean pushBlocks(O object,
    // int pushStart,
    // int pushEnd,
    // boolean left) {
    // ArrayList<ScheduleObject<O>> blocks = objectMap.get(object);
    // ArrayList<ScheduleObject<O>> blocksCopy =
    // (ArrayList<ScheduleObject<O>>) blocks.clone();
    // for (ScheduleObject<O> block : blocksCopy) {
    // int blockStart = block.getStart();
    // int blockEnd = block.getEnd();
    //
    // // todo: needs to be more general
    // //
    //
    // int push = 0;
    //
    // if (blockStart == pushStart && blockEnd == pushEnd) {
    // if (left)
    // push = -1;
    // else
    // push = +1;
    // }
    //
    // if (blockStart < pushStart && blockEnd >= pushStart) {
    // push = blockEnd - pushStart + 1;
    // push = -push;
    // }
    // else if (blockStart > pushStart && blockStart <= pushEnd) {
    // // push to right
    // push = pushEnd - blockStart + 1;
    // }
    //
    // if (push == 0)
    // continue;
    //
    // int newStart = blockStart + push;
    // int newEnd = blockEnd + push;
    //
    // if (newStart <= 0 || newEnd > numColumns)
    // return false;
    //
    //
    // removeBlock(block);
    // // recursive
    // ScheduleObject<O> newBlock = new ScheduleObject<O>(newStart,
    // newEnd, object);
    // boolean addedOk = addBlock(newBlock, false, left);
    // if (!addedOk) {
    // // add it back
    // addBlock(block);
    // return false;
    // }
    // }
    // return true;
    // }
    /**
     * Remove range from the schedule entirely for particular object.
     * 
     * @param removeStart
     * @param removeEnd
     * 
     */
    public void eraseRange(Object object, int removeStart, int removeEnd) {
        ArrayList<Integer> allocations = allocationMap.get(object);

        for (int i = removeStart; i <= removeEnd; i++)
            allocations.set(i, 0);
    }


    public void addAllocatorListener(AllocatorListener<O> listener) {
        support.addListener(listener);
    }


    public void removeBlockListener(AllocatorListener<O> listener) {
        support.removeListener(listener);
    }
}
